# -*- coding: utf-8 -*-

import remi
import remi.gui as gui
from remi.gui import *
from threading import Timer
import traceback
import time
import math
import epics
#from epics import caget, caput, cainfo

style_inheritance_dict = {'opacity':'inherit', 'overflow':'inherit', 'background-color':'inherit', 'background-image':'inherit', 'background-position':'inherit', 'background-repeat':'inherit', 'border-color':'inherit', 'border-width':'inherit', 'border-style':'inherit', 'border-radius':'inherit', 'color':'inherit', 'font-family':'inherit', 'font-size':'inherit', 'font-style':'inherit', 'font-weight':'inherit', 'white-space':'inherit', 'letter-spacing':'inherit'}
style_inheritance_text_dict = {'opacity':'inherit', 'overflow':'inherit', 'color':'inherit', 'font-family':'inherit', 'font-size':'inherit', 'font-style':'inherit', 'font-weight':'inherit', 'white-space':'inherit', 'letter-spacing':'inherit'}

# noinspection PyUnresolvedReferences
class EPICSWidget(object):
    @property
    @gui.editor_attribute_decorator('WidgetSpecific','The PV name', str, {})
    def epics_pv_name(self): return self.__epics_pv_name
    @epics_pv_name.setter
    def epics_pv_name(self, v): 
        self.__epics_pv_name = v
        self.disconnect()
        try:
            self.epics_pv = epics.PV(self.__epics_pv_name, auto_monitor=True, callback=self.onChanges, connection_callback=self.onConnectionChange, connection_timeout=2)
        except Exception:
            print(traceback.format_exc())
    
    epics_pv = None     # here will be stored the PV instance
    app_instance = None

    def __del__(self):
        self.disconnect()

    def disconnect(self):
        if self.epics_pv:
            self.epics_pv.clear_auto_monitor()
            self.epics_pv.disconnect()

    @decorate_set_on_listener("(self, emitter, pvname=None, conn=None, chid=None, **kwargs)")
    @decorate_event
    def onConnectionChange(self, pvname=None, conn=None, chid=None, **kwargs):
        #print('ca connection status changed:  ', pvname,  conn, chid)
        #Here I use the outline red color to show the unconnected state
        # of course this can be avoided or changed
        self.style['outline'] = "1px solid red"
        if conn:
            del self.style['outline']
        return (pvname, conn, chid, kwargs)

    @decorate_set_on_listener("(self, emitter, pvname=None, value=None, **kwargs)")
    @decorate_event
    def onChanges(self, pvname=None, value=None, **kwargs):
        #as default I write the value to the widget itself
        self.set_value(str(value))
        return (pvname, value, kwargs)

    def search_app_instance(self, node):
        if issubclass(node.__class__, remi.server.App):
            return node
        if not hasattr(node, "get_parent"):
            return None
        return self.search_app_instance(node.get_parent()) 

    def get_app_instance(self):
        if self.app_instance==None:
            self.app_instance = self.search_app_instance(self)
        return self.app_instance


class EPICSBooleanButton(gui.Container, EPICSWidget):
    """ A Button widget that sets the bit when clicked.
    """
    icon = ""

    @property
    @gui.editor_attribute_decorator('WidgetSpecific','Specifies if the button is toggle or must reset the value on release', bool, {})
    def toggle(self): return self.__toggle
    @toggle.setter
    def toggle(self, v): 
        self.__toggle = v
        self.button.onmouseup.do(self.reset_bit if not self.__toggle else None)

    @property
    @editor_attribute_decorator("WidgetSpecific",'''Text content''', str, {})
    def text(self): return self.button.get_text()
    @text.setter
    def text(self, value): self.button.set_text(value)

    button = None   # The gui.Button widget instance
    led = None      # The led indicator Widget

    def __init__(self, button_label='epics button', epics_pv_name='', toggle=False, *args, **kwargs):
        self.color_inactive = 'darkgray'
        self.color_active = 'rgb(0,255,0)'
        self.button = gui.Button(button_label, width="100%", height="100%", style=style_inheritance_dict)
        self.led = gui.Widget(width=15, height=5, style={'position':'absolute', 'left':'2px', 'top':'2px', 'background-color':self.color_inactive})
        self.led_status = False
        default_style = {'position':'absolute','left':'10px','top':'10px', 'background-color':'rgb(4, 90, 188)', 'color':'white'}
        default_style.update(kwargs.get('style',{}))
        kwargs['style'] = default_style
        kwargs['width'] = kwargs['style'].get('width', kwargs.get('width','100px'))
        kwargs['height'] = kwargs['style'].get('height', kwargs.get('height','100px'))
        super(EPICSBooleanButton, self).__init__(*args, **kwargs)
        _style = {'position':'relative'}
        _style.update(style_inheritance_dict)
        self.append(gui.Container(children=[self.button, self.led], width="100%", height="100%", style=_style))
        self.toggle = toggle
        self.epics_pv_name = epics_pv_name
        self.button.onmousedown.do(self.set_bit)

    def set_bit(self, emitter, *args, **kwargs):
        self.pressed = True
        self.written = False
        value = 1
        if self.toggle:
            value = 0 if self.led_status else 1
        self.epics_pv.put(value, callback = (self.put_done if not self.toggle else None) )

    def put_done(self, *args, **kwargs):
        self.written = True
        #this function gets called when a set_bit is completed and the button is not toggle
        # and so the value have to be reset
        if not self.pressed:
            self.epics_pv.put(0)

    def reset_bit(self, emitter, x, y, *args, **kwargs):
        self.pressed = False
        if self.written:
            self.epics_pv.put(0)

    def set_value(self, value):
        if not self.get_app_instance():
            return
        with self.get_app_instance().update_lock:
            #this function gets called when the camonitor notifies a change on the PV
            self.led_status = float(value)>0.0
            self.led.style.update({'background-color':self.color_active if self.led_status else self.color_inactive})


class EPICSLed(HBox, EPICSWidget):
    """A Status indicator widget.
    """
    icon = ""

    @property
    @editor_attribute_decorator("Geometry",'''Widget width.''', 'css_size', {})
    def css_width(self): return self.style.get('width', None)
    @css_width.setter
    def css_width(self, value): 
        self.style['width'] = str(value)
        self._update_size()

    @property
    @editor_attribute_decorator("Geometry",'''Widget height.''', 'css_size', {})
    def css_height(self): return self.style.get('height', None)
    @css_height.setter
    def css_height(self, value): 
        self.style['height'] = str(value)
        self._update_size()

    label_value = None # the gui.Label used to show the value 0 or 1

    def __init__(self, epics_pv_name='', *args, **kwargs):
        self.color_inactive = 'darkgray'
        self.color_active = 'rgb(0,180,0)'
        default_style = {'position':'absolute','left':'10px','top':'10px', 'color':'white','background-color':self.color_inactive, 'align-items':'center', 'justify-content':'center'}
        default_style.update(kwargs.get('style',{}))
        kwargs['style'] = default_style
        kwargs['width'] = kwargs['style'].get('width', kwargs.get('width','50px'))
        kwargs['height'] = kwargs['style'].get('height', kwargs.get('height','50px'))
        super(EPICSLed, self).__init__(*args, **kwargs)
        _style = {'text-align':'center'}
        _style.update(style_inheritance_text_dict)
        self.label_value = gui.Label("0", style=_style)
        self.append(self.label_value)
        self.epics_pv_name = epics_pv_name

    def _update_size(self):
        width = gui.from_pix(self.style.get('width', "100").replace("%",""))
        height = gui.from_pix(self.style.get('height', "100").replace("%",""))
        radius = min(width, height)/2
        self.style['border-radius'] = gui.to_pix(radius)

    def set_value(self, value):
        if not self.get_app_instance():
            return
        with self.get_app_instance().update_lock:
            _value = float(value)
            self.label_value.set_text( '1' if _value>0.0 else '0' )
            self.style.update({'background-color':self.color_active if _value>0.0 else self.color_inactive})

        
class EPICSValueMeterWidget(Progress, EPICSWidget):
    """A simple progress bar indicating a value.
    """
    icon = ""

    def __init__(self, epics_pv_name='', max_value=100, *args, **kwargs):
        default_style = {'position':'absolute','left':'10px','top':'10px'}        
        default_style.update(kwargs.get('style',{}))
        kwargs['style'] = default_style
        kwargs['width'] = kwargs['style'].get('width', kwargs.get('width','100px'))
        kwargs['height'] = kwargs['style'].get('height', kwargs.get('height','30px'))
        super(EPICSValueMeterWidget, self).__init__(0, max_value,*args, **kwargs)
        self.epics_pv_name = epics_pv_name

    def set_value(self, value):
        if not self.get_app_instance():
            return
        with self.get_app_instance().update_lock:
            Progress.set_value(self, value)


try:
    import pygal
except ImportError:
    print("It is required to install pygal to use EPICSPlotPV widget")

class EPICSPlotPV(gui.Svg, EPICSWidget):
    """A simple plot bar indicating a value.
        REQUIRES library pygal to be installed
    """
    icon = ""
    
    @property
    @editor_attribute_decorator("WidgetSpecific",'''Defines the maximum values count.''', int, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1})
    def max_values_count(self): return self.values.maxlen
    @max_values_count.setter
    def max_values_count(self, value): 
        self.values.maxlen = int(value)
        
    def __init__(self, epics_pv_name='', max_values_count=100, *args, **kwargs):
        w = kwargs.get("style", {}).get("width", kwargs.get("width", 100))
        h = kwargs.get("style", {}).get("height", kwargs.get("height", 100))
        if 'width' in kwargs.keys():
            del kwargs["width"]
        if 'height' in kwargs.keys():
            del kwargs["height"]
        default_style = {'position':'absolute','left':'10px','top':'10px', 'overflow':'hidden', 'background-color':'lightgray', 'margin':'10px'}
        default_style.update(kwargs.get('style',{}))
        kwargs['style'] = default_style
        super(EPICSPlotPV, self).__init__(w, h, *args, **kwargs)
        self.values = gui.SvgPolyline(max_values_count)
        self.epics_pv_name = epics_pv_name

    def set_value(self, value):
        if not self.get_app_instance():
            return
        with self.get_app_instance().update_lock:
            self.values.add_coord(time.clock(), float(value))
            try:
                plot = pygal.XY()
                pairs = []
                for i in range(0, len(self.values.coordsX)):
                    pairs.append([self.values.coordsX[i], self.values.coordsY[i]])
                plot.add(self.epics_pv_name, pairs)
                self.add_child("chart", plot.render())
            except Exception:
                self.style['overflow'] = "visible"
                self.add_child("chart", gui.SvgText(10,10, "Install pygal to use this widget"))


class EPICSValueGaugeWidget(Svg, EPICSWidget):
    """A gauge indicator for an EPICS PV value
    """
    icon = ""

    @property
    @editor_attribute_decorator("WidgetSpecific",'''Defines the minimum value.''', float, {'possible_values': '', 'min': -65535, 'max': 65535, 'default': 0, 'step': 1})
    def min_value(self): return self.__dict__.get('__min_value',0)
    @min_value.setter
    def min_value(self, value): 
        self.__dict__['__min_value'] = value
        self.text_min_value.set_text(str(value))

    @property
    @editor_attribute_decorator("WidgetSpecific",'''Defines the maximum value.''', float, {'possible_values': '', 'min': -65535, 'max': 65535, 'default': 0, 'step': 1})
    def max_value(self): return self.__dict__.get('__max_value',1)
    @max_value.setter
    def max_value(self, value): 
        self.__dict__['__max_value'] = value
        self.text_max_value.set_text(str(value))

    indicator = None        # a gui.SvgPolygon that indicates the actual value
    indicator_pin = None    # a gui.SvgCircle around which the indicator rotates
    text_min_value = None   # the gui.SvgText min value indicator
    text_max_value = None   # the gui.SvgText max value indicator
    text_actual_value = None    # the gui.SvgText value indicator

    def __init__(self, epics_pv_name='', min_value=0, max_value=100, *args, **kwargs):
        w = kwargs.get("style", {}).get("width", kwargs.get("width", 100))
        h = kwargs.get("style", {}).get("height", kwargs.get("height", 100))
        if 'width' in kwargs.keys():
            del kwargs["width"]
        if 'height' in kwargs.keys():
            del kwargs["height"]
        default_style = {'position':'absolute','left':'10px','top':'10px'}
        default_style.update(kwargs.get('style',{}))
        kwargs['style'] = default_style
        super(EPICSValueGaugeWidget, self).__init__(width=w, height=h, *args, **kwargs)
        self.epics_pv_name = epics_pv_name

        #the indicator
        self.indicator = gui.SvgPolygon(_maxlen=4)
        self.indicator.set_stroke(width=0.001, color='red')
        self.indicator.set_fill('red')

        indicator_pin_radius = 0.05
        self.indicator_pin = gui.SvgCircle(0,0.5,indicator_pin_radius)
        self.indicator_pin.set_fill('black') 

        #the value signs
        scale = max_value-min_value
        radius_min = 0.4
        radius_max = 0.5
        for i in range(0,10):
            angle = math.pi/9*i
            #sign = gui.SvgLine(math.cos(angle)*radius_min, radius_max-math.sin(angle)*radius_min, math.cos(angle)*radius_max, radius_max-math.sin(angle)*radius_max)
            sign = gui.SvgLine(math.cos(angle)*(radius_min - 0.01 + 0.1*(i+1)/10), radius_max-math.sin(angle)*(radius_min - 0.01 + 0.1*(i+1)/10), math.cos(angle)*radius_max, radius_max-math.sin(angle)*radius_max)
            sign.set_stroke(0.01, 'black')
            self.append(sign)

        #subindicators value signs
        scale = max_value-min_value
        radius_min = 0.4
        radius_max = 0.5
        for i in range(0,100):
            angle = math.pi/99*i
            #sign = gui.SvgLine(math.cos(angle)*radius_min, radius_max-math.sin(angle)*radius_min, math.cos(angle)*radius_max, radius_max-math.sin(angle)*radius_max)
            sign = gui.SvgLine(math.cos(angle)*(radius_min - 0.01 + 0.1*(i+10)/100), radius_max-math.sin(angle)*(radius_min - 0.01 + 0.1*(i+10)/100), math.cos(angle)*radius_max, radius_max-math.sin(angle)*radius_max)
            sign.set_stroke(0.002, 'black')
            self.append(sign)

        font_size = 0.1
        self.text_min_value = gui.SvgText(-radius_max, 0.5 + font_size + 0.01, str(min_value))
        self.text_min_value.style['font-size'] = gui.to_pix(font_size)
        self.text_min_value.style['text-anchor'] = "start"

        self.text_max_value = gui.SvgText(radius_max, 0.5 + font_size + 0.01, str(max_value))
        self.text_max_value.style['font-size'] = gui.to_pix(font_size)
        self.text_max_value.style['text-anchor'] = "end"

        self.text_actual_value = gui.SvgText(0, 0.5 + indicator_pin_radius + font_size + 0.01, str(max_value))
        self.text_actual_value.style['font-size'] = gui.to_pix(font_size)
        self.text_actual_value.style['text-anchor'] = "middle"
        self.text_actual_value.style['font-weight'] = 'bold'

        self.min_value = min_value
        self.max_value = max_value

        self.append([self.indicator, self.indicator_pin, self.text_min_value, self.text_max_value, self.text_actual_value])

        self.set_viewbox(-0.5, 0, 1, 0.70)
        self.value = self.min_value

    def set_value(self, value):
        if not self.get_app_instance():
            return
        with self.get_app_instance().update_lock:
            value = float(value)
            #min value at left
            #max value at right
            
            #value to radians
            scale = self.max_value-self.min_value
            if scale==0.0:
                return
            relative_value = value - self.min_value
            angle = relative_value*math.pi/scale
            #inversion min at left
            angle = math.pi - angle

            radius = 0.5
            self.indicator.add_coord(math.cos(angle)*radius, radius-math.sin(angle)*radius)
            self.indicator.add_coord(math.cos(angle+0.5)*0.04, radius-math.sin(angle+0.5)*0.04) #self.indicator.add_coord(0.02,0.4)
            self.indicator.add_coord(0,radius)
            self.indicator.add_coord(math.cos(angle-0.5)*0.04, radius-math.sin(angle-0.5)*0.04) 

            if hasattr(self, "actual_value"):
                self.text_actual_value.set_text(str(value))
